package com.project.gobang.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.gobang.constants.Constants;
import com.project.gobang.game.*;
import com.project.gobang.model.UserInfo;
import com.project.gobang.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;

// 这个类就用来处理客户端发来的游戏对战 websocket 请求
@Component
public class GameAPI extends TextWebSocketHandler {
    // 使用 ObjectMapper 将Java对象转成 JSON 格式字符串
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private RoomManager roomManager;

    // 维护用户在游戏房间的在线状态, 主要用于判定用户是否多开
    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private UserService userService;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 先构建一个响应, 方便随时返回
        ReadyToGameResponse response = new ReadyToGameResponse();

        // 1.获取用户的信息(从 WebSocketSession 中拿到, WebSocketSession 又从 HttpSession 中拿)
        UserInfo user = (UserInfo) session.getAttributes().get(Constants.USER);
        if (user == null) {
            // 说明用户并未登录
            response.setSuccess(false);
            response.setReason("用户未登录");
            // 将 JSON字符串 包装成 WebSocketMessage 类型传回前端
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }

        // 2.判定当前用户是否已经进入房间.(通过房间管理器来查找)(这里暂时不太理解,因为是跟着老师打的,老师有相关经验)
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            response.setSuccess(false);
            response.setReason("用户并未进入游戏房间");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }

        // 3. 判定当前账号是否多开.(防止用户从其他地方进入游戏房间)
        WebSocketSession hallOnlineStatus = onlineUserManager.getUserOnlineStatusInHallSession(user.getUserId());
        WebSocketSession roomOnlineStatus = onlineUserManager.getUserOnlineStatusInRoomSession(user.getUserId());
        if (hallOnlineStatus != null || roomOnlineStatus != null) {
            // 如果一个账号, 游戏大厅在线, 游戏房间还在线, 说明他多开了, 这也不行
            // 当然, 这里是如此, 那么游戏大厅 MatchAPI 那边也要如此.
            response.setSuccess(false);
            response.setReason("禁止多开账号");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }

        // 4. 前置判定都通过后, 就要把玩家上线进游戏房间了
        onlineUserManager.enterGameRoom(user.getUserId(), session);

        // 5. 把两个玩家加入到游戏房间中
        // 注意, 在 Matcher 中创建房间, 把玩家加入到游戏房间的过程中, 是在 game_hall 中完成的.
        // game_hall 匹配完成后, 是需要经过页面跳转来到 game_room 才算真正开始游戏.
        // 页面跳转这件事, 看似很小, 但在计算机中却是个大活, 我们是无法保证用户一定是跳转成功的.
        // 因此, 要在这里(game_room.html) 中, 确定玩家成功跳转了, 才把玩家的信息放到 Room 中.
        synchronized (room) {
            if (room.getPlayer1() == null) {
                // 如果玩家1尚未加入房间, 则把先连上服务器的玩家作为 player1 加入到游戏房间中
                room.setPlayer1(user);
                // 把先连入房间的玩家作为先手方
                room.setWhiteUserId(user.getUserId());
                System.out.println("玩家1: " + user.getUsername() + " 已经就绪" );
                return;
            }
            if (room.getPlayer2() == null) {
                // 走到这, 说明房间中玩家1已经连入服务器了, 现在是玩家2在连入服务器
                room.setPlayer2(user);
                System.out.println("玩家2: " + user.getUsername() + " 已经就绪" );

                // 当两个玩家都成功连入房间后, 就需要让服务器通知两个客户端, 给客户端返回 websocket 响应
                // 这里创建一个方法来通知
                // 通知玩家1(即,通知玩家1 所在的客户端这个房间及玩家信息)
                advicePlayerGameReady(room, room.getPlayer1(), room.getPlayer2());

                // 通知玩家2
                // 注意这里的玩家顺序, 谁是me, 谁是对手, 要分清楚
                advicePlayerGameReady(room, room.getPlayer2(), room.getPlayer1());
                return;
            }
        }
        // 6. 如果有第三个玩家尝试连接同一个房间, 提示报错
        // 正常情况下, 这种情况是不存在的, 为了让程序更加健壮, 还是做一个判定提示
        response.setSuccess(false);
        response.setReason("当前房间已满, 无法加入房间");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
    }

    private void advicePlayerGameReady(Room room, UserInfo me, UserInfo other) throws IOException {
        ReadyToGameResponse response = new ReadyToGameResponse();
        // me 表示本方, other 表示对手方
        response.setSuccess(true);
        response.setReason("");
        response.setMessage("readyToGame");
        response.setRoomId(room.getRoomId());
        response.setSelfPlayerId(me.getUserId());
        response.setAntiPlayerId(other.getUserId());
        response.setWhiteUser(room.getWhiteUserId());

        // 把构建好的 响应返回给客户端
        // 需要从在线状态管理器中, 拿到当前玩家对应的会话
        WebSocketSession webSocketSession = onlineUserManager.getUserOnlineStatusInRoomSession(me.getUserId());
        webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        // 1. 先从 session 中获取用户的信息(通过用户信息可以拿到房间信息)
        UserInfo user = (UserInfo) session.getAttributes().get(Constants.USER);
        if (user == null) {
            System.out.println("[GameAPI: handleMessage ]用户为空");
            return;
        }

        // 2. 根据玩家id 获取到房间对象
        Room room = roomManager.getRoomByUserId(user.getUserId());
        // 3. 通过 room 里面的方法来处理这次具体的 "落子" 请求
        // message.getPayload() 是拿到客户端传来的 "落子" 请求数据载荷
        room.putChess(message.getPayload().toString());

    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 在连接异常时进行的处理, 异常情况和下线情况处理方案一致, 让用户安全地下线
        // 需要将用户在维护用户在线状态的结构中移除
        UserInfo user = (UserInfo) session.getAttributes().get(Constants.USER);
        if (user == null) {
            // 一般来说 user 是不太可能为空的, 因为在前面已经判定过了
            // 这里只是为了保险起见
            System.out.println("用户为空, 连接异常情况");
            return;
        }

        // 要保证, 是同一个客户端(浏览器) 断开连接, 才能移除用户的在线状态
        WebSocketSession userOnlineSession = onlineUserManager.getUserOnlineStatusInRoomSession(user.getUserId());
        // session 表示当前连接到服务器的客户端 WebsocketSession
        // userOnlineSession 表示 user 用户登录时所用的客户端
        if (userOnlineSession == session) {
            // 两者一致, 才能把用户下线
            // 防止在多开的情况下, 第二个浏览器的退出操作, 把第一个浏览器的用户给整下线了
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("用户: " + user.getUsername() + " 游戏连接异常");

        // 如果本客户端掉线了, 就需要告诉本客户端的对手: 他赢了
        adviceAntiPlayerWin(user);
    }

    private void adviceAntiPlayerWin(UserInfo user) throws IOException {
        // 1.根据当前玩家, 找到房间, 通过房间找到对手信息
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            // 如果房间是空的, 说明这局游戏已经没了, 不作处理
            System.out.println("[GameAPI:error] 该房间已经被销毁");
            return;
        }
        // 2.找到对手信息
        // 如果当前用户是玩家1,那么对手就是玩家2; 如果当前用户是玩家2, 那么对手就是玩家1
        UserInfo antiPlayer = (user == room.getPlayer1()) ? room.getPlayer2() : room.getPlayer1();

        // 3.找到对手玩家的在线会话 session
        WebSocketSession session = onlineUserManager.getUserOnlineStatusInRoomSession(antiPlayer.getUserId());
        if (session == null) {
            // 如果对手也掉线了, 不作处理
            System.out.println("[GameAPI:] 对手也掉线了");
            return;
        }

        // 4.构造一个响应, 通知对手玩家所在的客户端
        PlaceChessResponse response = new PlaceChessResponse();
        response.setMessage("putChess");
        response.setUserId(antiPlayer.getUserId());
        response.setWinner(antiPlayer.getUserId());
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));

        // 5.如果本机客户端玩家掉线, 则认为本机玩家输掉比赛, 对手赢得比赛
        int winUserId = antiPlayer.getUserId();
        int loseUserId = user.getUserId();
        userService.winTheGame(winUserId);
        userService.loseTheGame(loseUserId);

        // 释放房间对象
        roomManager.remove(room.getRoomId(), room.getPlayer1().getUserId(), room.getPlayer2().getUserId());
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 在连接断开时进行的处理, 让用户安全地下线
        // 需要将用户在维护用户在线状态的结构中移除
        UserInfo user = (UserInfo) session.getAttributes().get(Constants.USER);
        if (user == null) {
            // 一般来说 user 是不太可能为空的, 因为在前面已经判定过了
            // 这里只是为了保险起见
            System.out.println("用户为空, 断开连接异常情况");
            // 断开连接后其实并没有处理多的操作,因此这里只是简单的 return, 不返回响应了
            return;
        }

        // 要保证, 是同一个客户端(浏览器) 断开连接, 才能移除用户的在线状态
        WebSocketSession userOnlineSession = onlineUserManager.getUserOnlineStatusInRoomSession(user.getUserId());
        // session 表示当前连接到服务器的客户端 WebsocketSession
        // userOnlineSession 表示 user 用户登录时所用的客户端
        if (userOnlineSession == session) {
            // 两者一致, 才能把用户下线
            // 防止在多开的情况下, 第二个浏览器的退出操作, 把第一个浏览器的用户给整下线了
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("用户: " + user.getUsername() + " 已下线");

        // 如果本客户端掉线了, 就需要告诉本客户端的对手: 他赢了
        adviceAntiPlayerWin(user);
    }
}

























